#
# Copyright (c) 2012 Philip Jameson
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without 
# modification, are permitted provided that the following conditions are met:
#
#  - Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#  - Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation 
#    and/or other materials provided with the distribution.
#  - Neither the name of the <ORGANIZATION> nor the names of its contributors 
#    may be used to endorse or promote products derived from this software 
#    without specific prior written permission.
#
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 
# THE POSSIBILITY OF SUCH DAMAGE.
#

import os
import binascii
from threading import Thread, Lock, Semaphore, Event
from collections import deque
import time
import socket
import select
import struct
import ssl
import time
import errno


class ApnsLog:
	"""Simple class that does a quick append of the app name and type iff the socket has a logger set. Only used internally in ApnsSocket instances"""
	def __init__(self, apnsSocket):
		self._apnsSocket = apnsSocket

	def critical(self, message):
		if self._apnsSocket.logger != None:
			self._apnsSocket.logger.critical('[%s/%s] %s' % (self._apnsSocket._appName, self._apnsSocket._socketTypeName, message))
	def warning(self, message):
		if self._apnsSocket.logger != None:
			self._apnsSocket.logger.warning('[%s/%s] %s' % (self._apnsSocket._appName, self._apnsSocket._socketTypeName, message))
	def info(self, message):
		if self._apnsSocket.logger != None:
			self._apnsSocket.logger.info('[%s/%s] %s' % (self._apnsSocket._appName, self._apnsSocket._socketTypeName, message))
	def debug(self, message):
		if self._apnsSocket.logger != None:
			self._apnsSocket.logger.debug('[%s/%s] %s' % (self._apnsSocket._appName, self._apnsSocket._socketTypeName, message))

class ApnsSocket(Thread):
	"""
	The ApnsSocket class is used to handle connections to Apple's push notification service

	The class itself maintains a list of applications that have been defined, as well as instances of ApnsSocket
	that are running for each application. This allows a single class method to find the correct thread to send
	messages to, and also allows for simple encapsulated thread/socket management
	"""

	"""The types of sockets that can be instantiated"""
	SOCKET_TYPE_PUSH = 0
	SOCKET_TYPE_FEEDBACK = 1

	"""Commands that Apple has implemented"""
	APNS_COMMAND_SIMPLE_PUSH = 0
	APNS_COMMAND_EXTENDED_PUSH = 1
	APNS_COMMAND_FEEDBACK = 8

	"""Errors taken from http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW2"""
	APNS_PUSH_ERRORS  = {
			0 : 'No errors encountered',
			1 : 'Processing error',
			2 : 'Missing device token',
			3 : 'Missing topic',
			4 : 'Missing payload',
			5 : 'Invalid token size',
			6 : 'Invalid topic size',
			7 : 'Invalid payload size',
			8 : 'Invalid token',
			255 : 'Unknown',
			}

	APNS_MAX_MESSAGE_LENGTH = 256 #The absolute limit of the message payload (excluding header information)
	APNS_DEVICE_TOKEN_LENGTH = 32 #Length of the device token in bytes. A hex string can be 64 chars

	APNS_HOSTNAME_DEV = 'gateway.sandbox.push.apple.com'
	APNS_HOSTNAME_PROD = 'gateway.push.apple.com'
	APNS_PORT_PUSH = 2195
	APNS_PORT_FEEDBACK = 2196

	globalMessageSockets = {} #The global dictionary of appName : ApnsSocket object mappings for push sockets
	globalFeedbackSockets = {} #The global dictionary of appName : ApnsSocket object mappings for feedback sockets
	_globalMessageLock = Lock() #Locks for the above dictionaries
	_globalFeedbackLock = Lock()

	"""
	Queues a message in the appropriate application's push socket (if available)

	Args:
		application: The name of the application as defined in the configuration file
		devices: A list of device tokens given as strings
		message: A JSON string of the message to send. Cannot be more than APNS_MAX_MESSAGE_LENGTH
			 See here: http://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1
		expiry: Unix timestamp when the message should be expired. Defaults to 0, which means just deliver the message
		identifier: An identifier to use for this batch of messages. If apple reports an error, this will be passed into your error handler

	Returns:
		True if the application was found and message was queued, False if the applcation was not found
	"""
	@classmethod
	def queueMessage(cls, application, devices, message, expiry = 0, identifier = 1):
		cls._globalMessageLock.acquire()

		ret = False
		if application in cls.globalMessageSockets:
			cls.globalMessageSockets[application].addMessage(devices, message, expiry, identifier)
			ret =  True

		cls._globalMessageLock.release()
		return ret

	"""
	Stop all running socket threads
	
	Returns:
		Nothing
	"""
	@classmethod
	def stopAll(cls):
		cls._globalMessageLock.acquire()
		for name, application in cls.globalMessageSockets.iteritems():
			application.stop()
		cls._globalMessageLock.release()
			
		cls._globalFeedbackLock.acquire()
		for name, application in cls.globalFeedbackSockets.iteritems():
			application.stop()
		cls._globalFeedbackLock.release()

		return

	"""
	Stops socket threads for a specific application

	Args:
		name - The application name given when the socket was created
		stopPush - Whether the push socket sould be stopped. Defaults to True
		stopFeedback - Whether the feedback socket sould be stopped. Defaults to True

	Returns:
		Nothing
	"""
	@classmethod
	def stopApplication(cls, name, stopPush = True, stopFeedback = True):
		if stopPush:
			cls._globalMessageLock.acquire()
			if name in cls.globalMessageSockets:
				cls.globalMessageSockets[name].stop()
				del(cls.globalMessageSockets[name])
			cls._globalMessageLock.release()
		if stopFeedback:
			cls._globalFeedbackLock.acquire()
			if name in cls.globalFeedbackSockets:
				cls.globalFeedbackSockets.stop()
				del(cls.globalFeedbackSockets[name])
			cls._globalFeedbackLock.release()

		return


	"""
	An instance of this class represents a single thread and single connection to Apple's Push Notification Service

	This class sets up a connection to either the Push Notification Service, or the Feedback Service, depending on the socket
	type specified. Only one socket of each type can be running for a given application at a time

	Attributes:
		messageFailureMethod: A method that handles failure messages that Apple sends when pushing messages. See ApnsSocket.messageFailed() for the method signature
		invalidatedDeviceMethod: A method that is called when a device is mentioned by the Feedback service. See ApnsSocket.invalidatedDevice() for the method signature
		logger: The logger object. If set, it must implement debug(str), info(str), critical(str) and warning(str)
	"""

	"""
	Creates an ApnsSocket instance

	Arguments:
		appName: The name of the application. This should be alphanumeric characters
		dev: Whether or not to connect to the development servers. If set to false, it will connect to the production servers
		socketType: The type of socket that should be created. One of ApnsSocket.SOCKET_TYPE_PUSH or ApnsSocket.SOCKET_TYPE_FEEDBACK
		certificateFile: The certificate file provided by Apple to connect to their servers
		keyFile: If the key and certificate are in different files, this is the file with the key in it. Default: None
		caFile: The location of the file with the CA certificates in it. This is used if the Entrust certificates are not in your root store. Default: None
		feedbackFrequency: The number of seconds to wait in between connections to the feedback service. Only applies to feedback sockets. Default: 60 seconds
	
	Raises:
		Exception - Various exceptions can be thrown during setup if invalid data is given. The only four originally are:
				Invalid socket type
				Invalid certificate file location
				Invalid key file location (if given)
				Invalid CA certificate file location (if given)
	"""
	def __init__(self, appName, dev, socketType, certificateFile, keyFile = None, caFile = None, feedbackFrequency = 60):
		if(socketType != self.SOCKET_TYPE_PUSH and socketType != self.SOCKET_TYPE_FEEDBACK):
			raise Exception('Invalid ApnsSocket socket type')

		if not os.path.exists(certificateFile):
			raise Exception('Invalid certificate file given for application %s' % appName)

		if keyFile != None and not os.path.exists(keyFileFile):
			raise Exception('Invalid keyFile file given for application %s' % appName)

		Thread.__init__(self)

		if dev:
			self._serverAddress = self.APNS_HOSTNAME_DEV
		else:
			self._serverAddress = self.APNS_HOSTNAME_PROD

		if socketType == self.SOCKET_TYPE_PUSH:
			self._serverPort = self.APNS_PORT_PUSH
			self._socketTypeName = 'push'
		elif socketType == self.SOCKET_TYPE_FEEDBACK:
			self._serverPort = self.APNS_PORT_FEEDBACK
			self._socketTypeName = 'feedback'


		self._appName = appName
		self._certificateFile = certificateFile
		self._keyFile = keyFile
		self._caFile = caFile
		self._socketType = socketType
		self._keepRunning = True
		self._maxRetries = 3
		self._feedbackFrequency = feedbackFrequency

		self._stopEvent = Event() #Triggered by stop() to let the thread know it needs to exit as soon as possible
		self._stoppedEvent = Event() #Sent by the thread to let stop() know that it has stopped

		self._messageQueue = deque() 
		self._messageLock = Lock()
		self._messageSemaphore = Semaphore(0)

		#Methods to call when either a message failed to send, or a device was invalidated
		self.messageFailureMethod = self.messageFailed
		self.invalidatedDeviceMethod = self.invalidatedDevice

		#Logging
		self.logger = None
		self._log = ApnsLog(self)

	"""
	Starts the thread and the loop for the socket. This also is where duplicate application verification  happens
	
	Returns:
		Nothing
	Raises:
		Exception: A socket of the specified type has already been started. Only one socket of each type 
			   can be run per application
	"""
	def start(self):
		ex = None

		if self._socketType == self.SOCKET_TYPE_PUSH:
			self._globalMessageLock.acquire()
			if self._appName in self.globalMessageSockets:
				ex =  Exception('A message socket for application %s already exists' % self._appName)
			else:
				self.globalMessageSockets[self._appName] = self
			self._globalMessageLock.release()

		elif self._socketType == self.SOCKET_TYPE_FEEDBACK:
			self._globalFeedbackLock.acquire()
			if self._appName in self.globalFeedbackSockets:
				ex =  Exception('A feedback socket for application %s already exists' % self._appName)
			else:
				self.globalFeedbackSockets[self._appName] = self
			self._globalFeedbackLock.release()
	
		if ex != None:
			raise ex	

		Thread.start(self)

	"""
	Actually starts the loops for a socket. This is invoked in its own thread by start() and should not be invoked independently
	"""
	def run(self):
		try:
			if self._socketType == self.SOCKET_TYPE_FEEDBACK:
				return self._runFeedback()
			elif self._socketType == self.SOCKET_TYPE_PUSH:
				return self._runPush()
		except Exception, e:
			self._log.critical('An unhandled exception occurred: %s' % str(e))
			self._keepRunning = False

	"""
	Checks the Apple feedback service periodically

	This method checks the Apple feedback service  periodically as specified in the constructor's
	feedbackFrequency variable. If a device token is marked as expired, it calls the method assigned
	to the socket's 'invalidatedDeviceMethod' variable. If any connection errors occur, the thread
	retries a set number of times and then will wait feedbackFrequency seconds before trying to connect
	again. This method is called by run() and should not be invoked directly
	"""
	def _runFeedback(self):
		lastFeedbackUpdate = 0

		sock = None
		secureSock = None
		
		connected = False #Whether or not we're even connected anymore
		shutdown = True #Whether or not we need to try to shutdown the socket at the end of the loop

		self._log.info('Started feedback thread')
		while(self._keepRunning):
			retries = 0
			keepReading = True

			invalidated = []

			self._log.debug('Connecting to feedback service')
			while retries < self._maxRetries:
				try:
					ai = socket.getaddrinfo(self._serverAddress, self._serverPort)
					sock = socket.socket()
					secureSock = ssl.wrap_socket(sock, certfile = self._certificateFile, keyfile = self._keyFile, ca_certs = self._caFile)
					secureSock.connect(ai[0][4])
					secureSock.settimeout(None) #Disable the timeout. We'll just wait for data to come in
					connected = True
					self._log.debug('Connected to feedback service')
					break
				except socket.error, e:
					self._log.warning('Could not connect to feedback service')
					self._log.debug('Socket exception when connecting to feedback service: %s' % str(e))
				except ssl.SSLError, e:
					self._log.warning('Could not connect to feedback service')
					self._log.debug('SSL Exception when connecting to feedback service: %s' % str(e))

				retries += 1
				time.sleep(1)
				self._log.info('Trying to connect to feedback service again')

			if connected == False:
				self._log.warning('Could not connect to feedback server for %s after %s attempts. An attempt will be made again in %s seconds' % (self._maxRetries, self._feedbackFrequency))
				keepReading = False
				
			self._log.info('Checking feedback')
			while keepReading:
				try:
					data = secureSock.recv(6)

					if len(data) == 0:
						self._log.debug('No more devices received from feedback service')
						keepReading = False
						shutdown = False
					elif len(data) == 6:
						self._log.debug('Got data from feedback service')
						(timestamp, deviceTokenLength) = struct.unpack('!ih', data)
						data = secureSock.recv(deviceTokenLength)

						if len(data) == 0:
							self._log.warning('Did not receive full device information before feedback service closed connection')
							keepReading = False
							shutdown = False
						elif len(data) == deviceTokenLength:
							(deviceTokenPacked,) = struct.unpack('!' + str(deviceToeknLength) + 's', data)
							deviceToken = binascii.b2a_hex(deviceTokenPacked)
							invalidated.append(deviceToken)

				except socket.error, e:
					self._log.warning('Got error when trying to receive data from the feedback service')
					self._log.debug('Socket exception when receiving from feedback service: %s' % str(e))
					keepReading = False
				except ssl.SSLError, e:
					self._log.warning('Got error when trying to receive data from the feedback service')
					self._log.debug('SSL exception when receiving from feedback service: %s' % str(e))
					keepReading = False
			
			#This is not used at the moment, but could be used for statistics later on
			lastFeedbackUpdate = time.time()

			if connected:
				if shutdown:
					try:
						secureSock.shutdown(socket.SHUT_RDWR)
					except socket.error, e:
						self._log.warning('Got an error when shutting down feedback service socket')
						self._log.debug('Socket exception when shutting down feedback service socket: %s' % str(e))
					except ssl.SSLError, e:
						self._log.warning('Got an error when shutting down feedback service socket')
						self._log.debug('SSL exception when shutting down feedback service socket: %s' % str(e))

				try:
					secureSock.close()
					connected = False
				except socket.error, e:
					self._log.warning('Got an error when closing feedback service socket')
					self._log.debug('Socket exception when closing feedback service socket: %s' % str(e))
				except ssl.SSLError, e:
					self._log.warning('Got an error when closing feedback service socket')
					self._log.debug('SSL exception when closing feedback service socket: %s' % str(e))

			for deviceToken in invalidated:
				self._log.info('%s: Invalidating device token "%s"' % (self._appName, deviceToken))
				self.invalidatedDeviceMethod(self._appName, deviceToken)

			self._stopEvent.wait(self._feedbackFrequency)

		#Let the stop() function know that we've exited our main loop
		self._stoppedEvent.set()

	"""
	Send messages to Apple's push notification service

	This method waits for messages to come in from the 'addMessage()' method, and pushes them off to the
	Push Notification Service. It will keep trying to connect until it succeeds, and if an error occurs while
	sending, will attempt to only resend to devices that weren't sent to yet. Any errors received from Apple
	will trigger the method assigned to the socket's 'messageFailureMethod' variable. In addition, the socket is
	held open between messages, and only closed if Apple closes it, or it times out for some reason
	"""
	def _runPush(self):
		connected = False

		sock = None
		secureSock = None
		connected = False

		self._log.info('Started push thread')
		while(self._keepRunning):
			self._messageSemaphore.acquire() #This is released (incremented) whenever a new message comes in, and also when the stop() method is called on the thread (since it's blocking)

			if self._keepRunning == False:
				break

			message = None
			self._messageLock.acquire()
			#Shouldn't happen, but make sure that we actually have a message in the queue
			if(len(self._messageQueue)):
				message = self._messageQueue[0]
			self._messageLock.release()

			if message != None:
				self._log.debug('Sending message to push service')
				retrySending = True #If we lose our connection part way through the send, then we need to finish sending to any devices that didn't get the message
				while retrySending:
					#Keep trying to connect indefinitely. If we can't connect with this message, we're not going to be able to connect with any other message either
					while connected == False:
						retries = 0
						try:
							ai = socket.getaddrinfo(self._serverAddress, self._serverPort)
							if len(ai) > 0:
								sock = socket.socket()
								secureSock = ssl.wrap_socket(sock, certfile = self._certificateFile, keyfile = self._keyFile, ca_certs = self._caFile)
								secureSock.connect(ai[0][4])
								secureSock.settimeout(None) #Disable the timeout. 
								connected = True
								self._log.debug('Connected to push service')
								break
							else:
								self._log.warning('Could not lookup "%s"')
								sleepTime = 5
						except socket.error, e:
							self._log.warning('Could not connect to push service')
							self._log.debug('Socket exception when connecting to push service: %s' % str(e))
							sleepTime = 1
						except ssl.SSLError, e:
							self._log.warning('Could not connect to push service')
							self._log.debug('SSL Exception when connecting to push service: %s' % str(e))
							sleepTime = 5

						retries += 1
						if self._stopEvent.wait(sleepTime): #Called from stop() 
							break
						self._log.info('Trying to connect to push service again')

					if self._keepRunning == False:
						break


					packFormatString = '!BIIH%ssH%ss' % (self.APNS_DEVICE_TOKEN_LENGTH, len(message['message']));
					handledIdx = 0 #This is how far we got before there was a socket error. APNS errors are sent to the error handling method, and considered dealt with
					for device in message['devices']:
						self._log.debug('Sending message to device with token "%s"' % device)

						binaryDeviceId = binascii.a2b_hex(device)
						#Format: COMMAND|IDENTIFIER|EXPIRATION TIME|DEVICE TOKEN LENGTH|DEVICE ID|MESSAGE LENGTH|MESSAGE
						packedPayload = struct.pack(packFormatString, self.APNS_COMMAND_EXTENDED_PUSH, message['identifier'], message['expiry'], self.APNS_DEVICE_TOKEN_LENGTH, binaryDeviceId, len(message['message']), message['message'])
						sent = 0
						try:
							while(sent < len(packedPayload)):
								sent += secureSock.send(packedPayload)
						except socket.error, e:
							if e[0] == errno.EPIPE:
								self._log.warning('Push service closed connection when trying to send a message')
								self._log.debug('Push service closed connection when trying to send a message to device %s' % device)
							else:
								self._log.warning('Push service encountered an error while sending a messgae')
								self._log.debug('Socket exception when sending to device %s via push service: %s' % (device, str(e)))
							connected = False
						except ssl.SSLError, e:
							self._log.warning('Push service encountered an error while sending a messgae')
							self._log.debug('SSL exception when sending to device %s via push service: %s' % (device, str(e)))
							#Try to close out the connection, because I have no idea where in the stack the error occurred
							try:
								secureSock.close()
							except:
								pass
							connected = False

						if connected == False:
							break

						#No real indication is given when you send data to the push service that there was an error, other than receiving some data. Timeout to .5 second, as 
						#it's not like apple has slow servers, and the protocol is really thin
						ready = select.select([secureSock], [], [], .5)
						if ready[0]:
							errorData = secureSock.recv(6)
							if len(errorData) == 0: #Not really sure how this would happen, but hey, check anyways
								self._log.warning('Push service closed connection when trying to send a message')
								self._log.debug('Push service closed connection after we sent to device %s, but before we could receive the error data back' % device)
								connected = False
								break
							elif len(errorData) == 6:
								(command, error, identifier) = struct.unpack('!BBI', errorData)
								if command == self.APNS_COMMAND_FEEDBACK:
									self._log.debug('Got message while sending to device %s' % device)
									self.messageFailureMethod(self._appName, identifier, device, message['message'], error, int(time.time()))
							else:
								self._log.warning('Got invalid data back from push service')
								self._log.debug('Got an invalid number of bytes in the receive after sending data to push service. Length was %s, data was %r' % (len(data), data))

						handledIdx += 1

					if connected == False:
						#Clear any devices that we sent a message to successfully
						self._log.warning('Sent message to %s devices, but lost the connection during send' % handledIdx)
						del message['devices'][:handledIdx]
					else:
						self._log.info('Successfully sent message to %s devices' % len(message['devices']) )
						self._messageLock.acquire()
						try:
							self._messageQueue.popleft()
						except:
							#This shouldn't really happen, but I don't really care if it does
							pass
						self._messageLock.release()
						retrySending = False

			else:
				self._log.debug('Message queue is empty. Waiting for next message')

		if secureSock and connected:
			try:
				secureSock.shutdown(socket.SHUT_RDWR)
			except socket.error, e:
				self._log.warning('Got an error when shutting down push service socket')
				self._log.debug('Socket exception when shutting down push service socket: %s' % str(e))
			except ssl.SSLError, e:
				self._log.warning('Got an error when shutting down push service socket')
				self._log.debug('SSL exception when shutting down push service socket: %s' % str(e))

			try:
				secureSock.close()
				connected = False
			except socket.error, e:
				self._log.warning('Got an error when closing push service socket')
				self._log.debug('Socket exception when closing push service socket: %s' % str(e))
			except ssl.SSLError, e:
				self._log.warning('Got an error when closing push service socket')
				self._log.debug('SSL exception when closing push service socket: %s' % str(e))

		self._stoppedEvent.set()

	"""
	Try to stop the loops started in the socket's run() method
	
	Returns:
		If the thread signaled that it was done, return True.
		If the thread already should be stopped, or if it fails to signal, return False
	"""
	def stop(self):
		ret = False
		if self._keepRunning == True:
			self._log.info('Stopping thread, please wait')
			self._keepRunning = False
			self._stopEvent.set()
			if self._socketType == self.SOCKET_TYPE_PUSH:
				self._messageSemaphore.release()
			ret = self._stoppedEvent.wait(self._feedbackFrequency)
			self._log.debug('Stopped thread')
		return ret

	"""
	Add a message to a socket's internal message queue.

	This will add a message to a socket's internal message queue. It is normally called from
	the class method 'queueMessage()', and generally doesn't need to be called manually

	Args:
		devices: A list of device tokens given as strings
		message: A JSON string of the message to send. Cannot be more than APNS_MAX_MESSAGE_LENGTH
			 See here: http://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1
		expiry: Unix timestamp when the message should be expired. Defaults to 0, which means just deliver the message
		identifier: An identifier to use for this batch of messages. If apple reports an error, this will be passed into your error handler
	"""
	def addMessage(self, devices, message, expiry, identifier):
		if self._socketType == self.SOCKET_TYPE_PUSH:
			self._messageLock.acquire()
			self._messageQueue.append({'devices' : devices, 'message' : message, 'identifier' : identifier, 'expiry' : expiry})
			self._messageSemaphore.release()
			self._messageLock.release()
		else:
			self._log.warning('Tried to send a message to a non-push socket')

	"""
	A sample method to use when Apple reports an error with our message

	Args:
		appName: The name of the application, as specified in the socket's constructor
		identifier: If the message had an identifier, this is it. It should always be an integer
		deviceToken: The string representation of the device token. Given in addMessage() or queueMessage()
		message: The JSON string of the message that was attempted
		reason: An integer reason that Apple gave for the failure. String representations can be found in 
			ApnsSocket.APNS_PUSH_ERRORS[errorCode]
		timestamp: Unix timestamp of when we got the error from Apple
	"""
	def messageFailed(self, appName, identifier, deviceToken, message, reason, timestamp):
		self._log.warning('Message \'%s\' could not be sent at %s. Reason was: %s' % (message, timestamp, self.APNS_PUSH_ERRORS[reason]))

	"""
	A sample method to use when the feedback service tells us that a device token was invalidated

	Args:
		appName: The name of the application, as specified in the socket's constructor
		deviceToken: The string representation of the device token. Given in addMessage() or queueMessage()
	"""
	def invalidatedDevice(self, appName, deviceToken):
		self._log.warning('Received device token %s from the feedback service' % deviceToken)

	"""
	A simple accessor to get the application name
	"""
	def name(self):
		return self._appName
